package it.tukano.lang;

import it.tukano.collections.HashMapExt;
import it.tukano.collections.MapExt;
import it.tukano.text.TextFormat;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.Calendar;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.text.JTextComponent;

/**
 * Immutable string wrapper with some utility
 * @author pgi
 */
public class StringExt {
    public static StringExt EMPTY = new StringExt("");
    
    /**
     * Checks if the given string is non null and non empty
     */
    public static boolean isDefined(String s) {
        return s != null && !s.isEmpty();
    }
    
    public static StringExt wrapUp(String string) {
        return string == null ? wrap(string) : wrap(string.toUpperCase());
    }

    public static StringExt wrap(String string) {
        return new StringExt(string);
    }
    
    public static StringExt wrap(Object obj) {
        return new StringExt(obj);
    }
    
    public static StringExt wrap(JTextComponent c) {
        return new StringExt(c.getText());
    }
    
    private final String string;

    /**
     * Instance initializer
     * @param string the string to wrap
     */
    public StringExt(String string) {
        this.string = string;
    }

    /**
     * Instance initializer
     * @param object the object to wrap (uses String.valueOf)
     */
    public StringExt(Object object) {
        this.string = String.valueOf(object);
    }

    /**
     * Convert a list of name=value name=value name=value pairs into a map
     * of string ext. The names are the keys of the map
     * @param separator the token used to separate properties (like , or space or
     * whatever)
     * @return a map of key-value pairs
     */
    public MapExt<String, StringExt> toProperties(String separator) {
        MapExt<String, StringExt> map = new HashMapExt<String, StringExt>();
        String[] tokens = string.split(separator);
        for (int i = 0; i < tokens.length; i++) {
            String token = tokens[i];
            String[] keyValue = token.split("=");
            map.put(keyValue[0], new StringExt(keyValue[1]));
        }
        return map;
    }

    /**
     * Returns the integer value of the string or the default value if parsing
     * fails
     * @param defaultValue the value to return if this wrapped doesn't contain
     * an integer value
     * @return the parsed value or the default one
     */
    public Integer toInteger(Integer defaultValue) {
        try {
            return Integer.parseInt(string);
        } catch(NumberFormatException ex) {
            return defaultValue;
        }
    }

    /**
     * Returns the wrapped string
     * @return the wrapped string
     */
    @Override
    public String toString() {
        return string;
    }
    
    /** Returns the wrapped string or the default value if the wrapped string is null */
    public String toString(String defaultValue) {
        return string == null ? defaultValue : string;
    }

    /**
     * Convert the wrapped string into an enum value
     * @param <T> the type of the enum
     * @param enumType the enum's class
     * @param defaultValue the value to return if parsing fails
     * @return the enum represented by this string or the default value
     */
    public <T extends Enum<T>> T toEnum(Class<T> enumType, T defaultValue) {
        try {
            return enumType.cast(enumType.getMethod("valueOf", String.class).invoke(null, toString()));
        } catch(Exception ex) {
            return defaultValue;
        }
    }

    /**
     * Converts the wrapped string into an enum or null if wrapping fails
     * @param <T> the type of the enum
     * @param enumType the class of the enum
     * @return the enum represented by this string or null if parsing fails
     */
    public <T extends Enum<T>> T toEnum(Class<T> enumType) {
        try {
            return enumType.cast(enumType.getMethod("valueOf", String.class).invoke(null, toString()));
        } catch(Exception ex) {
            return null;
        }
    }

    /**
     * Convert the wrapped string into a boolean
     * @param defaultValue the value to return if parsing fails
     * @return the parsed boolean or defaultValue
     */
    public Boolean toBoolean(Boolean defaultValue) {
        try {
            return Boolean.parseBoolean(string);
        } catch(Exception ex) {
            return defaultValue;
        }
    }

    /**
     * Convert the wrapped string into an integer
     * @return the parsed integer or null if parsing fails
     */
    public Integer toInteger() {
        return toInteger(null);
    }

    /**
     * Converts the wrapped string into a big decimal
     * @param defaultValue the value to return if parsing fails
     * @return the parsed big decimal or default value if parsing fails
     */
    public BigDecimal toBigDecimal(BigDecimal defaultValue) {
        try {
            String s = string;//currency case
            if(s.indexOf(",") >= 0) {
                s = s.replace(".", "").replace(",", ".");
            }
            return new BigDecimal(s);
        } catch(Exception ex) {
            return defaultValue;
        }
    }

    /**
     * Converts the wrapped string into a big integer
     * @param defaultValue the value to return if parsing fails
     * @return the parsed big integer or default value if parsing fails
     */
    public BigInteger toBigInteger(BigInteger defaultValue) {
        try {
            return new BigInteger(string);
        } catch(Exception ex) {
            return defaultValue;
        }
    }

    /**
     * Formats the wrapped string into a currency string (n.nnn.nnn,nn)
     * @param defaultValue the string to return if formatting fails
     * @return the wrapped string as a currency or default value if formatting
     * fails
     */
    public String getAsCurrency(String defaultValue) {
        String s = string;
        try {
            if(s.contains(",")) {
                s = s.replace(",", "a");
                s = s.replace(".", "");
                s = s.replace("a", ".");
            }
            BigDecimal d = new BigDecimal(s).setScale(2, RoundingMode.HALF_EVEN);
            return TextFormat.getInstance().asCurrencyString(d);
        } catch(Exception ex) {
            return defaultValue;
        }
    }
    
    public String getAsDate(String defaultValue) {
        Calendar c = toDate(null);
        if(c == null) {
            return defaultValue;
        }
        
        int day = c.get(Calendar.DAY_OF_MONTH);
        int month = c.get(Calendar.MONTH) + 1;
        int year = c.get(Calendar.YEAR);
        return day + "." + month + "." + year;
    }

    /**
     * Formats the wrapped string into a big decimal string (nnnnnn.nnnnnnn)
     * @param defaultValue the value to return if formatting fails
     * @return the wrapped string as a big decimal string or defaultValue if
     * formatting fails
     */
    public String getAsBigDecimal(String defaultValue) {
        try {
            return new BigDecimal(string).toString();
        } catch(Exception ex) {
            return defaultValue;
        }
    }

    /**
     * Formats the wrapped string into a big integer string (nnnnn)
     * @param defaultValue the value to return if formatting fails
     * @return the wrapped string as a big integer string or default value
     * if formatting fails
     */
    public String getAsBigInteger(String defaultValue) {
        try {
           return new BigInteger(string).toString();
        } catch(Exception ex) {
            return defaultValue;
        }
    }

    /**
     * Returns the wrapped string if the characters are all digits
     * @param defaultValue the value to return if one or more chars in the
     * wrapped string are not digits
     * @return the wrapped string or default value
     */
    public String getAsNumeric(String defaultValue) {
        if(string != null && !string.isEmpty()) for(int i = 0; i < string.length(); i++) {
            if(!Character.isDigit(string.charAt(i))) return defaultValue;
        }
        return string;
    }

    /**
     * Same as String.split but returns the tokens as StringExt wrappers
     * @param regEx the regular expression to use to split the string
     * @return the tokens. Can have 0 length, never null
     */
    public StringExt[] split(String regEx) {
        if(string == null) return null;
        
        String[] split = string.split(regEx);
        if(split == null) {
            return new StringExt[0];
        }
        StringExt[] tokens = new StringExt[split.length];
        for (int i = 0; i < split.length; i++) {
            String s = split[i];
            tokens[i] = new StringExt(s);
        }
        return tokens;
    }

    /**
     * Returns the value of the wrapped string parsed as a date in day-month-year
     * format. Splits the string using non digit chars as separators. Eg. 1.2.3
     * returns 1 february 0003, as 1#####2abcdfg3.4574765
     * @param defaultValue the value to return if formatting fails
     * @return the parsed time or the default value if parsing fails
     */
    public Calendar toDate(Calendar defaultValue) {
        
        StringExt[] tokens = split("\\D");
        if(tokens == null) return defaultValue;
        
        if(tokens.length == 3) try {
            int day = tokens[0].toInteger();
            int month = tokens[1].toInteger();
            int year = tokens[2].toInteger();
            Calendar c = Calendar.getInstance();
            c.set(Calendar.DAY_OF_MONTH, day);
            c.set(Calendar.MONTH, month - 1);
            c.set(Calendar.YEAR, year);
            return c;
        } catch(Exception ex) {
            return defaultValue;
        }
        return defaultValue;
    }
    
    public Calendar toDate() {
        return toDate(null);
    }

    public Properties toProperties() {
        Properties p = new Properties();
        try {
            p.load(new StringReader(string));
        } catch (IOException ex) {
            Logger.getLogger(StringExt.class.getName()).log(Level.SEVERE, null, ex);
        }
        return p;
    }

    public boolean isEmpty() {
        return string == null || string.isEmpty();
    }

    public boolean isDefined() {
        return !isEmpty();
    }

    public boolean isBigDecimal() {
        return getAsBigDecimal(null) != null;
    }

    public boolean leq(String provincia) {
        if(isEmpty()) return false;
        if(provincia == null) return false;
        return string.toLowerCase().equals(provincia.toLowerCase());
    }

    public File asFile(boolean checkExists) {
        if(isEmpty()) return null;
        File file = new File(string);
        if(checkExists) return file.exists() ? file : null;
        return file;
    }

    /**
     * Ensure that the wrapped string has the given prefix and suffix text and
     * returns it. If the wrapped string is null the text "[undefined]" will be used.
     * @param prefix the requested prefix, can be null
     * @param suffix the requested suffix, can be null
     * @return a string starting with prefix, followed by the content of this wrapper, 
     * ending with suffix.
     */
    public String asStringWithPrefixAndSuffix(String prefix, String suffix) {
        String wrappedString = this.string;
        if(wrappedString == null) wrappedString = "[undefined]";
        if(prefix != null && !wrappedString.startsWith(prefix)) { wrappedString = prefix + wrappedString; }
        if(suffix != null && !wrappedString.endsWith(suffix)) { wrappedString = wrappedString + suffix; }
        return wrappedString;
    }

    /**
     * Returns a string with the same text of the wrapped one minus a set of tokens
     * @param tokensToRemove the list of values to remove from the string
     * @return the stripped string
     */
    public String toStrippedString(String... tokensToRemove) {
        String stripped = string;
        for (int i = 0; i < tokensToRemove.length; i++) {
            String token = tokensToRemove[i];
            stripped = stripped.replace(token, "");
        }
        return stripped;
    }
    
    public StringExt removePrefix(String prefix) {
        int i = string.indexOf(prefix);
        if(i == 0) {
            String token = string.substring(prefix.length());
            return StringExt.wrap(token);
        } else {
            return this;
        }
    }

    public StringExt removeSuffix(String txt) {
        if(string.endsWith(txt)) {
            int i = string.lastIndexOf(txt);
            return StringExt.wrap(string.substring(0, i));
        } else {
            return this;
        }
    }

    public StringExt filterNonDigits() {
        String digits = "0123456789";
        StringBuilder buffer = new StringBuilder();
        for (int i = 0; i < string.length(); i++) {
            String s = string.substring(i, i+1);
            if(digits.indexOf(s) >= 0) buffer.append(s);
        }
        return StringExt.wrap(buffer);
    }
    
    public StringExt convertXmlEscapeTokens() {
        String s = string.replace("&amp;", "&");
        return StringExt.wrap(s);
    }
    
    public static String wrapIntoCDATASection(String text) {
        text = "<![CDATA[" + text + "]]>";
        return text;
    }
    
    public static void main(String args[]) {
        String s = "045.2356_lii4568";
        System.out.println(StringExt.wrap(s).filterNonDigits());
    }

    public String getAsBigDecimal(String text, int scale) {
        try {
            return new BigDecimal(text).setScale(scale, RoundingMode.HALF_EVEN).toString();
        } catch(NumberFormatException ex) {
            return text;
        }
    }
}
